package lumis.portal.webfilereplication;

import lumis.portal.filesystem.PortalFile;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;

/**
 * File replication that interacts with a target repository using FTP.
 * 
 * @version $Revision: 13093 $ $Date: 2011-05-28 18:40:18 -0300 (Sat, 28 May 2011) $
 * @since 6.0.0
 */
public class FTPFileReplicator extends AbstractFileReplicator
{
	/**
	 * The FTP client interface.
	 * @since 6.0.0
	 */
	private FTPClient client;

	/**
	 * The host of the FTP server.
	 * @since 6.0.0
	 */
	private String host;

	/**
	 * The port of the FTP server.
	 * @since 6.0.0
	 */
	private int port;

	/**
	 * The user name used on the server login.
	 * @since 6.0.0
	 */
	private String username;

	/**
	 * The password used on the server login.
	 * @since 6.0.0
	 */
	private String password;

	/**
	 * The folder that will store the files.
	 * @since 6.0.0
	 */
	private String targetFolder;

	/**
	 * The result of command <code>PWD</code> just after login.
	 * 
	 * @since 6.0.0
	 */
	private String userDirectory;

	/**
	 * Default constructor.
	 * 
	 * @since 6.0.0
	 */
	public FTPFileReplicator()
	{
		client = new FTPClient();
	}

	@Override
	public void initialize() throws FileReplicationException
	{
		try
		{
			readWebFileReplicationProperties();
			client.setConnectTimeout(60000);
			client.setDefaultTimeout(60000);
			client.setDataTimeout(60000);
			client.connect(host, port);
			client.login(username, password);
			client.setFileType(FTP.BINARY_FILE_TYPE);

			
			  // reads user directory
			if (FTPReply.isNegativePermanent(client.pwd()))
				throw new FileReplicationException(
						"Couldn't obtain user's current directory. Reply: "
								+ client.getReplyString());
			userDirectory = getUserDirectoryFromReply(client.getReplyString());
		}
		catch (FileReplicationException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new FileReplicationException(e);
		}
	}

	@Override
	public void dispose() throws FileReplicationException
	{
		try
		{
			client.logout();
		}
		catch (Exception e)
		{
			throw new FileReplicationException(e);
		}
	}

	/**
	 * Tries to read the current working directory from reply of
	 * <code>PWD</code> command.
	 * 
	 * @param pwdReply
	 *            the reply of <code>PDW</code> command.
	 * @return the user current directory or null if couldn't read it.
	 * @since 6.0.0
	 */
	private String getUserDirectoryFromReply(String pwdReply)
	{
		pwdReply = pwdReply.substring(3).trim();
		Pattern p = Pattern.compile("^\"([^\"]+)\".*");
		Matcher m = p.matcher(pwdReply);
		String pwd;
		if (m.matches())
			pwd = m.group(1);
		else
			pwd = pwdReply;

		return pwd;
	}

	public void write(PortalFile file) throws FileReplicationException
	{
		try
		{
			if (file.isDirectory())
				makeDirsAndCwd(getRemoteFilePath(file));
			else
			{
				makeDirsAndCwd(getRemoteFilePath(file.getParentPortalFile()));
				writeRemoteFile(file);
			}
		}
		catch (IOException e)
		{
			throw new FileReplicationException(e);
		}
	}

	public void delete(PortalFile file) throws FileReplicationException
	{
		String dirsPath = getRemoteFilePath(file.getParentPortalFile());
		try
		{
			StringBuilder aggregateCurrentDir = new StringBuilder();
			if (dirsPath.startsWith("/"))
			{
				aggregateCurrentDir.append("/");
				client.changeWorkingDirectory("/");
				dirsPath = dirsPath.substring(1);
			}
			else
			{
				aggregateCurrentDir.append(userDirectory);
				if (!(userDirectory.endsWith("/")))
					aggregateCurrentDir.append("/");
			}
			String[] dirs = dirsPath.split("/");
			for (String dir : dirs)
			{
				// try to change cwd to dir
				if (!(client.changeWorkingDirectory(aggregateCurrentDir.toString() + dir)))
				{
					// if any parent dir doesn't exists, the file doesn't exists
					// either.
					// nothing to do.
					return;
				}
				else
				{
					aggregateCurrentDir.append(dir + "/");
				}
			}

			// since we could enter the parent directory, try to delete file
			if (file.isDirectory())
			{
				// if directory doesn't exists, do nothing
				if (!(client.changeWorkingDirectory(file.getName())))
					return;

				// directory exists, cwd to parent..
				client.changeToParentDirectory();
				if (!(client.removeDirectory(file.getName())))
					throw new FileReplicationException("Could not remove directory '"
							+ aggregateCurrentDir.toString() + file.getName() + "'");
			}
			else
			{
				boolean fileExists = false;

				// Gets info of the FTP Server
				String result = client.getStatus(file.getName());

				// Results started width 2 represents success.
				if ((result != null) && (result.startsWith("2")))
				{
					result = result.replace("\r", "");
					// According with RFC 959, result with multiple lines have
					// '-'
					if (result.charAt(3) == '-')
					{
						// the return are multiple lines
						if (result.split("\n").length > 2)
						{
							fileExists = true;
						}
					}
					else
					{
						// the return is a single line
						if (result.substring(4, result.length()).length() > 2)
						{
							fileExists = true;
						}
					}
				}

				if (!fileExists)
					return;

				if (!client.deleteFile(file.getName()))
					throw new FileReplicationException("Could not remove file '"
							+ aggregateCurrentDir + file.getName() + "'");
			}
		}
		catch (IOException e)
		{
			throw new FileReplicationException(e);
		}
	}

	/**
	 * Writes remote file.
	 * 
	 * @param file
	 *            the file to be written.
	 * @throws FileReplicationException
	 * @throws IOException
	 * @since 6.0.0
	 */
	private void writeRemoteFile(PortalFile file) throws FileReplicationException, IOException
	{
		// write the file (overwriting if it already exist)
		FileInputStream local = new FileInputStream(file);
		try
		{
			if (!client.storeFile(file.getName(), local))
				throw new FileReplicationException("Could not write file '" + file.getName() + "'");
		}
		finally
		{
			local.close();
		}
	}

	/**
	 * Make directories (if necessary) and <code>CWD</code> into them.
	 * 
	 * @param remoteFilePath
	 *            the remote file path.
	 * @throws IOException
	 * @throws FileReplicationException
	 * @since 6.0.0
	 */
	private void makeDirsAndCwd(String remoteFilePath) throws IOException, FileReplicationException
	{
		StringBuilder aggregateCurrentDir = new StringBuilder();
		if (remoteFilePath.startsWith("/"))
		{
			aggregateCurrentDir.append("/");
			client.changeWorkingDirectory("/");
			remoteFilePath = remoteFilePath.substring(1);
		}
		else
		{
			aggregateCurrentDir.append(userDirectory);
			if (!(userDirectory.endsWith("/")))
				aggregateCurrentDir.append("/");
		}
		String[] dirs = remoteFilePath.split("/");
		for (String dir : dirs)
		{
			// try to change cwd to dir
			if (!(client.changeWorkingDirectory(aggregateCurrentDir.toString() + dir)))
			{
				// try to create dir
				if (!(client.makeDirectory(dir)))
					throw new FileReplicationException("Couldn't create directory "
							+ aggregateCurrentDir + dir);

				// change cwd to created dir
				client.changeWorkingDirectory(aggregateCurrentDir + dir);
				aggregateCurrentDir.append(dir + "/");
			}
			else
			{
				aggregateCurrentDir.append(dir + "/");
			}
		}
	}

	/**
	 * Given a local file, returns the remote file path.
	 * 
	 * @param file
	 *            the local file.
	 * @return the remote file path.
	 * @since 6.0.0
	 */
	private String getRemoteFilePath(PortalFile file)
	{
		String ret = "";
		if (targetFolder != null)
		{
			ret = targetFolder + "/";
		}
		if (file != null)
			ret += file.getPortalFilePath().getRelativePath();

		return ret.replace("//", "/");
	}


	/**
	 * Reads the properties from the {@link #getWebFileReplication()} into this
	 * instance attributes.
	 * 
	 * @throws FileReplicationException
	 *             if the properties are not properly set.
	 * 
	 * @since 6.0.0
	 */
	private void readWebFileReplicationProperties() throws FileReplicationException
	{
		// clear properties
		host = null;
		port = 21;
		username = null;
		password = null;
		
		// read properties
		Collection<WebFileReplicationPropertyValue> values = getWebFileReplication().getPropertyValues();
		for (WebFileReplicationPropertyValue customPropertyValue : values)
		{

			String name = customPropertyValue.getProperty().getName();
			String value = null;

			if (customPropertyValue.getValue() != null && customPropertyValue.getValue().trim().length() > 0)
			{
				value = customPropertyValue.getValue();
			}
			else
			{
				// no value
				continue;
			}

			if ("Host".equals(name))
			{
				this.host = value;
			}
			else if ("Port".equals(name))
			{
				this.port = Integer.parseInt(value);
			}
			else if ("Username".equals(name))
			{
				this.username = value;
			}
			else if ("Password".equals(name))
			{
				this.password = value;
			}
			else if ("Target Folder".equals(name))
			{	
				if (value != null)
				{
					value = value.replace("\\", "/");
					if (value.endsWith("/"))
					{
						value = value.substring(0,value.length()-1);
					}
					this.targetFolder = value;
				}
			}
		}
		
		// check properties
		if (host == null)
			throw new FileReplicationException("Value for 'host' property not found in " + getWebFileReplication().getName() + ".");
		if (username == null)
			throw new FileReplicationException("Value for 'username' property not found in " + getWebFileReplication().getName() + ".");
		if (password == null)
			throw new FileReplicationException("Value for 'password' property not found in " + getWebFileReplication().getName() + ".");
	}
}
